查看原文
其他

Cocos Creator 3D 渲染管线超强解读!

Kunkka COCOS 2022-06-10
作者对于 3.0 渲染管线的理解非常深入。这篇文章可以帮助开发者快速熟悉 3.0 的渲染管线,尽快熟悉下一代渲染架构。
——Cocos Minggo

  作者:Kunkka

大家好!我是一名来自腾讯的 Cocos 开发者,从 Cocos-iPhone,Cocos2d-x lua,Cocos Creator 到 Cocos Creator 3D,算下来我使用 Cocos 引擎有差不多10年了。此前比较少写博客,这是第一次在 Cocos 社区写技术分享,欢迎大家在评论区或社区原帖与我进行交流!


欢迎关注我的 Github

https://github.com/kunka

前言

Cocos Creator 3D 刚刚发布了 3.0 Preview 版,首次将 2D 和 3D 版本合并到了一起,经过多个版本迭代,渲染架构大幅升级与优化,非常值得深入学习和研究,以下是官网的性能与框架介绍:

  • 多渲染后端框架,已支持 WebGL 1.0 和 WebGL 2.0
  • 面向未来的底层渲染 API 设计
  • 基于 Command Buffer 提交渲染数据

目前渲染相关文档并不完善,本文将从源码入手分析 Cocos Creator 3D 多渲染后端框架 GFX,引擎默认的前向渲染管线实现,以及如何实现自定义渲染管线

目录

  • 多渲染后端框架 GFX
    • WebGL 渲染过程
    • GFX 中的对象
    • GFX 渲染过程
  • Cocos Creator 3D 渲染管线
    • 渲染架构 UML
    • 前向渲染管线
    • 自定义渲染管线


多渲染后端框架 GFX

GFX 是针对渲染层做的高级抽象和封装,以达到编写一次渲染代码,适配不同渲染后端的目的。

目前源码中可见引擎已经适配了以下几种渲染后端:

  • WebGL
  • WebGL2
  • OpenGL ES2
  • OpenGL ES3
  • Metal
  • Vulkan

GFX 可以理解为实现 Cocos Creator 3D 引擎渲染的最基础接口,实现自定义渲染只能通过 GFX 提供的接口和规则来编写,以往在 Cocos 引擎中直接编写 GL 代码的方式已经成为过去。

GFX 接口设计更贴近 Vulkan 等下一代渲染接口,为了说明 GFX 如何抽象渲染层,我们通过WebGL 渲染做一个对比,然后再用 GFX 接口来实现同样的功能。

WebGL 渲染过程

下面的示例代码展示了一次简单的 WebGL 渲染,目的是显示一张 2D 纹理:

function prepare(){
  // texture
  var texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);
  gl.texParameteri(/**...*/);
  gl.texImage2D(/** fill image data */);

  // shader
  var shader = someCreateShaderFunc("vert...""frag...");
  gl.useProgram(shader);

  // vertex buffer
  var vb = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, vb);
  gl.bufferData(/** fill vert data */;
  // bind attributes
  var attr = gl.getAttribLocation(shader, 'a_position');
  gl.enableVertexAttribArray(attr);
  gl.vertexAttribPointer(attr, /**...*/);
  var attr = gl.getAttribLocation(shader, 'a_texCoord');
  gl.enableVertexAttribArray(attr);
  gl.vertexAttribPointer(attr, /**...*/);

  // indices buffer
  var ib = gl.createBuffer();
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ib);
  gl.bufferData(/** fill indices data */;
}

prepare();
render();

function render(){
  // begin draw
  //gl.bindFramebuffer(gl.FRAMEBUFFER, /**...*/);

  gl.clearColor(/**...*/);
  gl.clear(gl.COLOR_BUFFER_BIT);
  gl.viewport(/**...*/);

  /** set states: depth test, stencil test, blend ... */
  gl.useProgram(shader);
  // set uniforms
  gl.uniform(/**...*/);

  // draw
  gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);

  // end draw
  //gl.bindFramebuffer(gl.FRAMEBUFFER, null);
  
  requestAnimationFrame(render);
}

这里可以把渲染过程简单分为2个阶段:数据准备渲染执行渲染执行又可分为数据绑定绘制调用。实际游戏中渲染执行中会反复执行多个 DrawCall,直到完成一帧中的所有绘制。

  • 数据准备:创建和提交数据到 GPU

    • 创建纹理
    • 创建并编译 Shader,也就是 WebGLProgram
    • 准备顶点数据,绑定 attributes layout
    • 准备索引数据
  • 渲染执行

    • 数据绑定:(Set Pass Call)
    • 绘制调用:(Draw Call)
    • 绑定纹理
    • 设置 uniform 参数(Shader 中的固定变量)
    • 调用绘制函数
    • 准备画布,清除颜色/深度缓冲,设置 viewport 等
    • 渲染结束

数据准备:可以理解为模型数据(顶点和纹理等)的上传;

渲染执行:每一帧都会调用,刷新游戏画面;

数据绑定:一般抽象为材质数据,切换数据绑定则相当于切换不同的材质。

GFX 中的对象

Device:抽象GFX 渲染设备,提供与设备交互的渲染接口,具体实例化为 WebGLDeviceVKDevice 等。

此外定义了15种渲染对象类型:

export enum ObjectType {
    UNKNOWN,
    BUFFER,
    TEXTURE,
    RENDER_PASS,
    FRAMEBUFFER,
    SAMPLER,
    SHADER,
    DESCRIPTOR_SET_LAYOUT,
    PIPELINE_LAYOUT,
    PIPELINE_STATE,
    DESCRIPTOR_SET,
    INPUT_ASSEMBLER,
    COMMAND_BUFFER,
    FENCE,
    QUEUE,
    WINDOW,
}

其中 Fence(同步信号)和 Window 在 WebGL 实现中并没有用到。TextureSamplerShaderFrameBuffer 比较好理解,跟 GL 对象差异不大,其他9种对象类型分别是:

  • Buffer:抽象 VB,IB,UB 等各种数据缓冲。
  • InputAssembler:集中管理 VB,IB,Attributes,IndirectBuffer 等各种输入。
  • DescriptorSet:集中管理 UB,Texture,Sampler 等。
  • DescriptorSetLayout:描述 DescriptorSet 绑定的布局。(相当于告诉 GPU 如何读取 DescriptorSet 数据)
  • CommandBuffer:将每一个渲染动作抽象为命令提交到队列,submit 时统一执行,支持异步渲染。(为 Vulkan 等下一代渲染接口准备,WebGL 并未实现)
  • Queue:CommandBuffer 队列?(WebgGL 实现只有一个空的 Queue,CommandBuffer 只用了一个默认的)
  • RenderPass:存放颜色缓冲区和深度缓冲区,也就是画布。
  • PipelineLayout DescriptorSetLayout和其他扩展信息,如 WebGL2 实现中有 WebGL2PipelineLayout 信息。
  • PipelineState:主要由以下状态动态组建
    • PipelineLayout
    • Shader
    • RenderPass
    • RasterizerState:CullMode,PolygonMode 等
    • DepthStencilState:DepthTest,StencilTest等
    • BlendTarget:Blend 设置
    • BlendState:BlendTarget集合 等
    • InputState:Attributes

DescriptorSet参考 Vulkan 中的概念:

GFX 渲染过程

基于上述定义的基础概念,如果使用 GFX 实现同样的功能,代码如下:

function prepare() {
  // vertex buffer
  const vertexBuffers = device.createBuffer(/** buffer info */);
  vertexBuffers.update(/** fill vert data */);
  // indices buffer
  const indicesBuffers = device.createBuffer(/** buffer info */);
  indicesBuffers.update(/** fill indices data */);
  // bind attributes
  const attributes: Attribute[] = [
    new Attribute('a_position', Format.RG32F),
    new Attribute('a_texCoord', Format.RG32F),
  ];
  const IAInfo = new InputAssemblerInfo(attributes, [vertexBuffers], indicesBuffers);
  const assmebler = device.createInputAssembler(IAInfo);

  // material, texture(sampler), shader, pass
  const material = new Material();
  material.initialize({ effectName: 'some shader name' });
  sampler = device.createSampler(/**samplerInfo*/);
  texture = device.createTexture(/** ...*/);

  const pass = material.passes[0];
  const binding = pass.getBinding('mainTexture');
  pass.bindTexture(bingding, texture);

  shader = ShaderPool.get(pass.getShaderVariant());
  const descriptorSet = DSPool.get(PassPool.get(pass.handle, PassView.DESCRIPTOR_SET));
  descriptorSet.bindSampler(binding, sampler);
  descriptorSet.update();
}
prepare();
render();
function render() {
  device.acquire();

  const cmdBuff = device.cmdBuff;
  const framebuffer = root.framebuffer;
  const renderArea = new Rect(0, 0, device.width, device.height);

  cmdBuff.begin();
  // bind framebuffer, clear, set states ...
  cmdBuff.beginRenderPass(framebuffer.renderPass, framebuffer, renderArea/** */);

  // bind PipelineState
  const pass = material.passes[0];
  const pso = PipelineStateManager.getOrCreatePipelineState(device, pass, shader, framebuffer.renderPass, assmebler);
  cmdBuff.bindPipelineState(pso);
  cmdBuff.bindDescriptorSet(SetIndex.MATERIAL, pass.descriptorSet);
  cmdBuff.bindInputAssembler(assmebler);
  // draw
  cmdBuff.draw(assmebler);

  cmdBuff.endRenderPass();

  cmdBuff.end();
  device.queue.submit([cmdBuff]);
  device.present();

  requestAnimationFrame(render);
}

整理一下 GFX 渲染流程:

  • 数据准备:创建和提交数据到 GPU
    • 创建 Material,初始化 Effect
    • 创建 Texture 和 Sampler,并绑定 Texture 到 Pass
    • 创建 InputAssembler
    • 创建 GFX Shader
    • 根据 Pass,从对象池获取 DescriptorSet, 绑定并更新
  • 提交渲染指令:准备画布 beginRenderPass
    • 调用绘制函数 draw(WebGL 直接绘制)
    • 获取 PSO 对象,绑定 PSO
    • 绑定 DescriptorSet
    • 绑定 IA
    • 数据绑定:(Set Pass Call)
    • 绘制调用:(Draw Call)
  • 执行渲染队列
    • 提交并执行 CommandBuffer

对比 WebGL 可以发现,渲染流程几乎一模一样,这有利于我们快速学习,但是细节上却有很大的区别,这也是号称面向未来的渲染 API 设计的原因,这里在 GFX 之上又封装了一些概念:

  • Effect:Cocos Creator 3D 独有语法的 Shader 原始文件,类似 Unity 的 ShaderLab。
  • Pass:包含 BlendState,RasterizerState 等所有信息,全部按位存于 handle 里面,非常的高效。
  • Material:对应一个 Effect,可以有多个 Pass。
  • Shader(GFX Shader):结合 pass 指定编译宏组合动态创建,非常灵活。

显然 GFX 抽象的接口使用起来更加方便和灵活,OpenGL 状态机只提供最细粒度的状态设置接口,如果渲染状态切换,OpenGL 需要设置一大堆标志位,现在可以直接切换 Pipeline,并且 Cocos Creator 3D 使用了非常多的对象池来优化性能。


Cocos Creator 3D 渲染管线

Cocos Creator 3D 渲染管线基于 GFX 接口,再次做了一层封装,方便应用层灵活使用,大致的渲染流程如下:

其中 Camera 数量可以有多个,Canvas(内含正交 OrthCamera)也可以有多个。FlowStage 都可以自定义和自由组合,Stage 负责执行具体渲染指令。

Cocos Creator 3D 渲染架构 UML

渲染相关类定义非常多,而且关系错综复杂,很多相互引用,这里列举一下几个关键类的含义:

  • Root:可以理解为渲染大总管,集中管理所有渲染相关的对象,包含 RenderPipeline,RenderWindow,RenderScene,Cameras,UI
  • RenderPipeline:渲染管线,定义一组 RenderFlow 队列
  • RenderWindow:渲染窗口,可以是屏幕缓冲也可以是离屏缓冲,可能有多个
  • RenderView:渲染视图,Camera 对象的渲染层表示
  • RenderScene:整个 Scene 场景对应的渲染层对象
  • UI:Scene 场景中所有 Canvas 对应的渲染对象,统一由 UI 管理,Cocos Creator 3D 单独为 UI 创建了一个 RenderScene 用于存放 UI 渲染模型 UIBatchedModel,所以 Root 中一共有2个 RenderScene
  • RenderFlow:定义一组渲染 Stage
  • RenderStage:渲染具体的实现,如 ForwardStage,UIStage

Cocos Creator 3D 前向渲染管线

前向渲染管线是 Cocos Creator 3D 提供的默认渲染管线,实现了3个类型的Flow

  • ShadowFlow:渲染阴影。
  • ForwardFlow:对应一个 3D Camera,可能有多个。
  • UIFlow:对应一个 ui_Canvas,可能有多个。

另外橙色步骤比较关键,主要负责性能优化:

  • GenBatchedModel:UI 动态合批。
  • SceneCulling:裁剪渲染对象。
  • FillQueue:根据裁剪后的对象,填充 Instanced 队列,Batched队列 ,不透明队列,透明队列。

Cocos Creator 3D 自定义染管线

Cocos Creator 3D 支持自定义渲染管线,我们尝试在 ForwPipeline中新建一个后处理 Flow,右键依次新建 Forward Pipeline AssetPostRenderFlowPostRenderStage

点击刚才创建的 Pipeline 资源,打开 Inspector,设置对应的 FlowStage,将 PostRenderFlow 插入 ForwardFlowUIFlow 之间。

然后重载 PostRenderFlowactivate 方法,实现 Flow 的初始化

public activate(pipeline: ForwardPipeline) {
    super.activate(pipeline);
    // create framebuffer
}

重载 PostRenderStagerender 方法,实现自定义渲染逻辑:

render(view: RenderView) {
  const pipeline = this._pipeline as ForwardPipeline;
  const cmdBuff = pipeline.commandBuffers[0];
  const device = pipeline.device;
  const renderPass = this.frameBuffer!.renderPass;

  cmdBuff.begin();
  cmdBuff.beginRenderPass();
  // insert custom render code here

  cmdBuff.endRenderPass();
  cmdBuff.end();
  bufs[0] = cmdBuff;
  device.queue.submit(bufs);
}

最后打开 Project Setting,切换至我们的 Pipeline,然后就可以运行了!

结束

Cocos Creator 的 3D 功能正在努力完善,后续会推出更多高级和实用功能,我们将在第一时间体验。

十分感谢一直免费开源的 Cocos,提供给我们直面源码的机会,祝福 Cocos 十周年生日快乐!

下一个十年更精彩!

参考文档

  • Cocos Creator 3D 用户手册:

    https://docs.cocos.com/creator3d/manual/zh/


  • Mozilla WebGL API:

    https://developer.mozilla.org/zh-CN/docs/Web/API/WebGL_API


  • learnopengl.com:

    https://learnopengl.com/


  • Vulkan 资源绑定和状态管理:

    https://zhuanlan.zhihu.com/p/172479225


非常感谢 Kunkka 带来的技术分享,欢迎各位开发者点击「阅读原文」查看原贴,为作者点赞,与作者进行交流学习!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存